﻿using SharpCompress.Archives;
using SharpCompress.Common;
using SharpCompress.Readers;
using System;
using System.IO;
using System.Linq;
using SharpCompress.Archives.GZip;
using SharpCompress.Compressors.BZip2;
using DotNetCommon.Extensions;
using SharpCompress.Archives.Zip;
using System.Collections.Generic;
using System.Text;

namespace DotNetCommon.Compress;

/// <summary>
/// 压缩和解压缩帮助类, 引用SharpZipLib
/// </summary>
public class CompressHelper
{
    #region 压缩文件
    private static void save(ZipArchive archive, Stream outputStream)
    {
        archive.SaveTo(outputStream, new SharpCompress.Writers.WriterOptions(CompressionType.Deflate)
        {
            ArchiveEncoding = new ArchiveEncoding(Encoding.UTF8, Encoding.UTF8),
        });
    }
    /// <summary>
    /// 从流中压缩到流,示例:
    /// <code>
    /// CompressHelper.CompressZip(destStream, new Dictionary&lt;string, Stream>
    /// {
    ///     {"中文B222.txt",stream1 },
    ///     {"testfolder/testsubfolder-suba222.txt",stream2 },
    ///     {"testfolder-a.txt",stream3 }
    /// });
    /// </code>
    /// </summary>
    public static void CompressZip(Stream outputStream, Dictionary<string, Stream> dic)
    {
        AssertUtil.NotNull(outputStream);
        AssertUtil.NotNullOrEmpty(dic);

        using var archive = ZipArchive.Create();
        foreach (var item in dic) archive.AddEntry(item.Key, item.Value);
        save(archive, outputStream);
    }

    /// <summary>
    /// 将指定多个文件压缩到一个文件,示例:
    /// <code>
    /// CompressHelper.CompressZip(destFilePath, new Dictionary&lt;string, string>
    /// {
    /// 	{"中文B222.txt","c:/testfolder/中文B.txt" },
    /// 	{"testsubfolder/suba222.txt","c:/testfolder/testsubfolder/testsubfolder-suba.txt" },
    /// 	{"testfolder-a.txt","c:/testfolder/testfolder-a.txt" }
    /// });
    /// </code>
    /// </summary>
    public static void CompressZip(string dest, Dictionary<string, string> originFiles)
    {
        AssertUtil.NotNullOrWhiteSpace(dest);
        AssertUtil.NotNullOrEmpty(originFiles);

        if (!dest.EndsWith(".zip", StringComparison.OrdinalIgnoreCase)) throw new Exception("后缀名必须是.zip!");
        var dir = Path.GetDirectoryName(dest);
        if (!Directory.Exists(dir)) Directory.CreateDirectory(dir);
        if (File.Exists(dest)) File.Delete(dest);

        using var fs = new FileStream(dest, FileMode.CreateNew);
        using var archive = ZipArchive.Create();
        foreach (var item in originFiles)
        {
            archive.AddEntry(item.Key, new FileInfo(item.Value));
        }
        save(archive, fs);
    }

    /// <summary>
    /// 递归压缩文件夹,示例:
    /// <code>
    /// CompressHelper.CompressZipFolder(destFilePath, new Dictionary&lt;string, string>
    /// {
    /// 	{"testfolder1","c:/test/testfolder") },
    /// 	{"testfolder2","c:/test2/testfolder") },
    /// });
    /// </code>
    /// </summary>
    public static void CompressZipFolder(string dest, Dictionary<string, string> orginFolders)
    {
        AssertUtil.NotNullOrWhiteSpace(dest);
        AssertUtil.NotNullOrEmpty(orginFolders);
        using var fs = new FileStream(dest, FileMode.Create);
        using var archive = ZipArchive.Create();
        foreach (var i in orginFolders)
        {
            var folder = i.Value.Replace("\\", "/").TrimEnd('/') + '/';
            var files = Directory.GetFiles(folder, "*", SearchOption.AllDirectories);
            foreach (var f in files)
            {
                var file = f[folder.Length..];
                var key = $"{i.Key.Replace("\\", "/").TrimEnd('/')}/{file}";
                archive.AddEntry(key, new FileInfo(f));
            }
        };
        save(archive, fs);
    }

    /// <summary>
    /// 递归压缩文件夹(将文件夹及文件夹内容放到压缩包内),示例:
    /// <code>
    /// CompressHelper.CompressZipFolder(destFilePath, "c:/testfolder");
    /// </code>
    /// </summary>
    /// <remarks>注意: 得到的压缩包内会有外层文件夹的名称,如果只想压缩(<c>orginFolder</c>)的内容,参考:<seealso cref="CompressZipFolderContent(string, string)"/></remarks>
    public static void CompressZipFolder(string dest, string orginFolder)
    {
        AssertUtil.NotNullOrWhiteSpace(dest);
        AssertUtil.NotNullOrWhiteSpace(orginFolder);
        var key = orginFolder.SplitAndTrim('/', '\\').LastOrDefault();
        CompressZipFolder(dest, new Dictionary<string, string>() { { key, orginFolder } });
    }

    /// <summary>
    /// 递归压缩文件夹(将文件夹内容放到压缩包内),示例:
    /// <code>
    /// CompressHelper.CompressZipFolderContent(destFilePath, "c:/testfolder");
    /// </code>
    /// </summary>
    public static void CompressZipFolderContent(string dest, string orginFolder)
    {
        AssertUtil.NotNullOrWhiteSpace(dest);
        AssertUtil.NotNullOrWhiteSpace(orginFolder);
        CompressZipFolder(dest, new Dictionary<string, string>() { { "", orginFolder } });
    }
    #endregion

    #region 解压缩文件
    private static ReaderOptions GetReaderOptions()
    {
        return new ReaderOptions
        {
            ArchiveEncoding = new ArchiveEncoding { CustomDecoder = (bs, start, end) => LuanMaHelper.GetString(bs.Skip(start).Take(end - start).ToArray()) }
        };
    }
    /// <summary>
    /// 解压缩文件到指定文件夹, 示例:
    /// <list type="bullet">
    /// <item> CompressHelper.UnCompress("c:\\test.zip","c:\\testdir") </item>
    /// <item> CompressHelper.UnCompress("c:\\test.rar","c:\\testdir") </item>
    /// <item> CompressHelper.UnCompress("c:\\test.7z","c:\\testdir") </item>
    /// <item> CompressHelper.UnCompress("c:\\test.tar","c:\\testdir") </item>
    /// <item> CompressHelper.UnCompress("c:\\test.tar.gz","c:\\testdir") </item>
    /// <item> CompressHelper.UnCompress("c:\\test.bz2","c:\\testdir") </item>
    /// <item> CompressHelper.UnCompress("c:\\test.tar.bz2","c:\\testdir") </item>
    /// </list>
    /// </summary>
    public static void UnCompress(string src, string destDir)
    {
        AssertUtil.NotNullOrWhiteSpace(src);
        AssertUtil.NotNullOrWhiteSpace(destDir);
        if (!Directory.Exists(destDir)) Directory.CreateDirectory(destDir);
        if (!File.Exists(src)) throw new FileNotFoundException($"原始文件未找到,无法解压({src})!");
        if (rarAndTar.Any(i => src.EndsWith(i, StringComparison.OrdinalIgnoreCase)))
        {
            using Stream stream = File.OpenRead(src);
            unAr(stream, destDir);
        }
        else if (src.EndsWith(".gz", StringComparison.OrdinalIgnoreCase))
        {
            if (src.EndsWith(".tar.gz", StringComparison.OrdinalIgnoreCase))
            {
                using var archive = GZipArchive.Open(src, GetReaderOptions());
                using var stream = archive.Entries.First().OpenEntryStream();
                unAr(stream, destDir);
            }
            else
            {
                //单文件
                using var archive = GZipArchive.Open(src, GetReaderOptions());
                using var stream = archive.Entries.First().OpenEntryStream();
                var tmpPath = Path.Combine(destDir, Path.GetFileNameWithoutExtension(src));
                using var fsDest = new FileStream(tmpPath, FileMode.CreateNew);
                stream.CopyTo(fsDest);
            }
        }
        else if (src.EndsWith(".bz2", StringComparison.OrdinalIgnoreCase))
        {
            var tmpPath = Path.Combine(destDir, Path.GetFileNameWithoutExtension(src));
            if (src.EndsWith(".tar.bz2", StringComparison.OrdinalIgnoreCase))
                tmpPath = Path.Combine(Path.GetDirectoryName(src), DateTime.Now.ToFileNameGuidString(".tar"));
            using (var fs = new FileStream(src, FileMode.Open))
            {
                var str = new BZip2Stream(fs, SharpCompress.Compressors.CompressionMode.Decompress, true);
                using var destFs = new FileStream(tmpPath, FileMode.CreateNew); str.CopyTo(destFs);
            }
            if (src.EndsWith(".tar.bz2", StringComparison.OrdinalIgnoreCase))
            {
                using (var stream = new FileStream(tmpPath, FileMode.Open))
                {
                    unAr(stream, destDir);
                }
                File.Delete(tmpPath);
            }
        }
        else if (zipAnd7z.Any(i => src.EndsWith(i, StringComparison.OrdinalIgnoreCase)))
        {
            using var archive = ArchiveFactory.Open(src, GetReaderOptions());
            foreach (var entry in archive.Entries)
            {
                if (!entry.IsDirectory)
                {
                    entry.WriteToDirectory(destDir, new ExtractionOptions() { ExtractFullPath = true, Overwrite = true });
                }
            }
        }
        else throw new Exception($"不支持的文件格式: {Path.GetExtension(src)}!");
    }

    private static readonly string[] rarAndTar = [".rar", ".tar"];
    private static readonly string[] zipAnd7z = [".zip", ".7z"];

    /// <summary>
    /// 解压缩流到指定文件夹, 示例:
    /// <list type="bullet">
    /// <item> CompressHelper.UnCompress(stream,"test.zip","c:\\testdir") </item>
    /// <item> CompressHelper.UnCompress(stream,"test.rar","c:\\testdir") </item>
    /// <item> CompressHelper.UnCompress(stream,"test.7z","c:\\testdir") </item>
    /// <item> CompressHelper.UnCompress(stream,"test.tar","c:\\testdir") </item>
    /// <item> CompressHelper.UnCompress(stream","test.tar.gz","c:\\testdir") </item>
    /// <item> CompressHelper.UnCompress(stream,"test.bz2","c:\\testdir") </item>
    /// <item> CompressHelper.UnCompress(stream,"test.tar.bz2","c:\\testdir") </item>
    /// </list>
    /// </summary>
    /// <remarks>注意: 需要传入原文件名( <c>srcFileName</c> ),这样才能选择正确的解压缩算法</remarks>
    public static void UnCompress(Stream stream, string srcFileName, string destDir)
    {
        AssertUtil.NotNull(stream);
        AssertUtil.NotNullOrWhiteSpace(destDir);
        AssertUtil.NotNullOrWhiteSpace(srcFileName);
        if (!Directory.Exists(destDir)) Directory.CreateDirectory(destDir);
        if (rarAndTar.Any(i => srcFileName.EndsWith(i, StringComparison.OrdinalIgnoreCase)))
        {
            unAr(stream, destDir);
        }
        else if (srcFileName.EndsWith(".gz", StringComparison.OrdinalIgnoreCase))
        {
            var archive = GZipArchive.Open(stream);
            //单文件
            var entry = archive.Entries.First();

            if (srcFileName.EndsWith(".tar.gz", StringComparison.OrdinalIgnoreCase))
            {
                using (stream = entry.OpenEntryStream())
                {
                    unAr(stream, destDir);
                }
            }
            else entry.WriteToFile(Path.Combine(destDir, Path.GetFileNameWithoutExtension(srcFileName)));
        }
        else if (srcFileName.EndsWith(".bz2", StringComparison.OrdinalIgnoreCase))
        {
            var tmpPath = Path.Combine(destDir, Path.GetFileNameWithoutExtension(srcFileName));
            if (srcFileName.EndsWith(".tar.bz2", StringComparison.OrdinalIgnoreCase))
                tmpPath = Path.Combine(Path.GetDirectoryName(srcFileName), DateTime.Now.ToFileNameGuidString(".tar"));
            var str = new BZip2Stream(stream, SharpCompress.Compressors.CompressionMode.Decompress, true);
            using (var destFs = new FileStream(tmpPath, FileMode.CreateNew)) str.CopyTo(destFs);
            if (srcFileName.EndsWith(".tar.bz2", StringComparison.OrdinalIgnoreCase))
            {
                using (stream = new FileStream(tmpPath, FileMode.Open))
                {
                    unAr(stream, destDir);
                }
                File.Delete(tmpPath);
            }
        }
        else if (zipAnd7z.Any(i => srcFileName.EndsWith(i, StringComparison.OrdinalIgnoreCase)))
        {
            var archive = ArchiveFactory.Open(stream, new ReaderOptions
            {
                ArchiveEncoding = new ArchiveEncoding { CustomDecoder = (bs, start, end) => LuanMaHelper.GetString(bs.Skip(start).Take(end - start).ToArray()) }
            });
            foreach (var entry in archive.Entries)
            {
                if (!entry.IsDirectory)
                {
                    entry.WriteToDirectory(destDir, new ExtractionOptions() { ExtractFullPath = true, Overwrite = true });
                }
            }
        }
        else throw new Exception($"不支持的文件格式: {Path.GetExtension(srcFileName)}!");
    }

    private static void unAr(Stream stream, string destDir)
    {
        if (!Directory.Exists(destDir)) Directory.CreateDirectory(destDir);
        var reader = ReaderFactory.Open(stream, new ReaderOptions
        {
            ArchiveEncoding = new ArchiveEncoding
            {
                CustomDecoder = (bs, start, end) => LuanMaHelper.GetString(bs.Skip(start).Take(end - start).ToArray())
            }
        });
        while (reader.MoveToNextEntry())
        {
            if (!reader.Entry.IsDirectory)
                reader.WriteEntryToDirectory(destDir, new ExtractionOptions() { ExtractFullPath = true, Overwrite = true });
        }
    }
    #endregion
}
